//-----------------------------------------------------------------------
// <copyright file="FileCollectionBase.cs" company="Alan Rodrigues">
//     Copyright (c) Alan Rodrigues. All rights reserved.
// </copyright>
// <developed>Alan Rodrigues (operninha@gmail.com)</developed>
//-----------------------------------------------------------------------

namespace FileCollection
{
    using System;
    using System.Collections;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Runtime.Remoting.Messaging;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Security.Cryptography;
    using System.Threading;

    /// <summary>
    ///  
    /// </summary>
    /// <typeparam name="C">Collection Type</typeparam>
    /// <typeparam name="T">Information Type </typeparam>
    public abstract class FileCollectionBase<C, T> : IDisposable where C : ICollection, new()
    {
        #region Constants
        /// private delegate U LoadFileHandler<U>(Guid id, bool deleteOnClose);
        protected const string CollectionExtension = "collection";
        protected const int DefaultBufferSize = 102400;
        protected const string ExtensionData = "dat";
        protected const string ExtensionInfo = "info";
        protected const string ExtensionNew = "new";
        protected const string ExtensionTemp = "tmp";
        protected const string HeaderCollectionName = "Name";
        protected const string HeaderCollectionType = "CollectionType";
        protected const string HeaderVersion = "Version";
        #endregion

        #region Variables
        private string collectionName;
        [NonSerialized] private object syncRoot;
        private bool automaticSave;
        private int bufferSize;
        private volatile bool closed;
        private DirectoryInfo directoryInfo;
        #endregion

        #region Constructors
        public FileCollectionBase(string directoryPath, string name, bool automaticSave)
        {
            if (string.IsNullOrEmpty(directoryPath))
            {
                throw new ArgumentNullException("directoryPath");
            }

            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            this.collectionName = name;
            this.directoryInfo = new DirectoryInfo(directoryPath);
            if (!this.directoryInfo.Exists)
            {
                this.directoryInfo.Create();
            }

            this.AutomaticSave = automaticSave;
            this.BufferSize = DefaultBufferSize;
            this.CollectionFileName = CollectionFileNameFormat(name);
            this.CollectionFullFileName = Path.Combine(this.directoryInfo.FullName, this.FileName);

            this.Collection = new C();

            FileInfo[] fileLists = this.directoryInfo.GetFiles(this.FileName);
            if (fileLists.Length == 0)
            {
               this.SaveCollection();
            }
            else if (fileLists.Length == 1)
            {
                if (fileLists[0].Name == this.CollectionFileName)
                {
                    this.LoadCollection();
                }
                else
                {
                    throw new ApplicationException("Already exists one collection saved in this diretory, and is allowed only one.");
                }
            }
            else
            {
                throw new ApplicationException("Exists more than one collection saved in this diretory, and is allowed only one.");
            }
        }
        #endregion

       #region Properties

        public int BufferSize
        {
            get
            {
                 return this.bufferSize;
            }

            set
            {
                this.ValidateException();
                this.bufferSize = value <= 0 ? DefaultBufferSize : value;
            }
        }

        public bool AutomaticSave
        {
            get
            {
                this.ValidateException();
                return this.automaticSave;
            }

            set
            {
                this.ValidateException();
                this.automaticSave = value;
            }
        }

        public virtual object SyncRoot
        {
            get
            {
                if (this.syncRoot == null)
                {
                    Interlocked.CompareExchange(ref this.syncRoot, new object(), null);
                }

                return this.syncRoot;
            }
        }

        public DirectoryInfo DirectoryInfo
        {
            get
            {
                this.ValidateException();
                return this.directoryInfo;
            }

            protected set
            {
                this.directoryInfo = value;
            }
        }

        public string Name
        {
            get
            {
                this.ValidateException();
                return this.collectionName;
            }
        }

        public string FileName
        {
            get
            {
                this.ValidateException();
                return this.CollectionFileName;
            }
        }

        public int Count
        {
            get
            {
                this.ValidateException();
                lock (this.SyncRoot)
                {
                    return this.Collection.Count;
                }
            }
        }

        protected int Version
        {
            get;
            set;
        }

        protected string CollectionFileName
        {
            get;
            set;
        }

        protected string CollectionFullFileName
        {
            get;
            set;
        }

        protected string CollectionType
        {
            get;
            set;
        }

        protected bool Disposed
        {
            get;
            set;
        }

        protected C Collection
        {
            get;
            set;
        }

        protected bool Closed
        {
            get { return this.closed; }
            set { this.closed = value; }
        }
        
        #endregion

        #region Public Methods

        public static FileInfo[] FindCollections(string directoryPath, SearchOption searchOption)
        {
            var rootPath = new DirectoryInfo(directoryPath);
            return rootPath.GetFiles(string.Format("*.{0}", CollectionExtension), searchOption);
        }

        public int Repair()
        {
            this.ValidateException();

            lock (this.SyncRoot)
            {
                int count = 0;

                this.CleanCollection();
                
                // verifica arquivos novos a partir dos .info
                // existe info mas nao existe dat
                count += this.ClearOrpheansFiles();

                // AGORA VERIFICA TODOS OS INFOS/DATS QUE NAO ESTAO NA QUEUE E ADICIONA POR ORDEM DE DATA CRIADA
                // VALIDA TAMBEM O HASH DOS DAT
                count += this.VerifyFilesInCollection();
                this.SaveCollection();
                return count;
            }
        }

        #endregion

        #region IDisposable Members

        public virtual void Dispose()
        {
            this.ValidateException();
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        #region Protecteds Mehotds

        protected static string CollectionFileNameFormat(string name)
        {
            return string.Format("{0}.{1}", name, CollectionExtension);
        }

        protected static FileInfo GetDataFileInfo(FileInfo fileInfo)
        {
            string strDataFileName = fileInfo.Name.Replace("." + ExtensionInfo, "." + ExtensionData);

            return new FileInfo(fileInfo.FullName.Replace(fileInfo.Name, strDataFileName));
        }

        protected static FileInfo GetInfoFileInfo(FileInfo fileInfo)
        {
            string strInfoFileName = fileInfo.Name.Replace("." + ExtensionData, "." + ExtensionInfo);

            return new FileInfo(fileInfo.FullName.Replace(fileInfo.Name, strInfoFileName));
        }
      
        // --------------------------- CopyStream ---------------------------        

        /// <summary>
        /// Copies data sourceName a source stream targetName a target stream.</summary>
        /// <param name="source">
        /// The source stream targetName copy sourceName.</param>
        /// <param name="target">
        /// The destination stream targetName copy targetName.</param>
        protected void CopyStream(Stream source, Stream target)
        {
            source.Seek(0, SeekOrigin.Begin);
            var buf = new byte[this.bufferSize];
            int bytesRead;
            while ((bytesRead = source.Read(buf, 0, this.bufferSize)) > 0)
            {
                target.Write(buf, 0, bytesRead);
            }
        } // end:CopyStream()
        
        protected void ValidateException()
        {
            if (this.Disposed)
            {
                throw new ObjectDisposedException(GetType().FullName);
            }

            if (this.closed)
            {
                throw new ApplicationException("The object is closed.");
            }
        }

        #endregion

        protected virtual void Dispose(bool disposing)
        {
            if (!this.Disposed)
            {
                if (disposing)
                {
                    this.SaveCollection();
                }
            }

            this.Disposed = true;
        }

        protected virtual void SaveCollection()
        {
            lock (this.SyncRoot)
            {
                this.directoryInfo.Refresh();
                if (!this.directoryInfo.Exists)
                {
                    this.directoryInfo.Create();
                }

                try
                {
                    var streamFile = new FileStream(this.CollectionFullFileName, FileMode.Create, FileAccess.Write, FileShare.Read, this.BufferSize, FileOptions.WriteThrough | FileOptions.SequentialScan);
                    var binForm = new BinaryFormatter { TypeFormat = FormatterTypeStyle.TypesWhenNeeded };
                    Header[] headers = this.GetHeaders();
                    binForm.Serialize(streamFile, this.Collection, headers);
                    streamFile.Close();
                }
                catch (IOException ex)
                {
                    Trace.TraceError("Error trying targetName save the collection. \n Detail: {0}", ex.Message);
                }
            }
        }

        protected virtual void LoadCollection()
        {
            lock (this.SyncRoot)
            {
                try
                {
                    var streamFile = new FileStream(this.CollectionFullFileName, FileMode.Open, FileAccess.Read, FileShare.Read, this.BufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan);
                    var binForm = new BinaryFormatter();
                    this.Collection = (C)binForm.Deserialize(streamFile, this.ReadHeader);
                    streamFile.Close();
                }
                catch (SerializationException)
                {
                    Trace.TraceWarning("The collection is corrupted, trying targetName recovering");
                    this.SaveCollection();
                    this.Repair();
                }
            }
        }

        protected ExtraInformation<T> GetExtraInfo(Guid id, T info, Stream stream)
        {
            var extraInfo = new ExtraInformation<T>
                                {
                                    Id = id,
                                    ExtraInfo = info,
                                    Date = DateTime.Now,
                                    RecoveryInfo = new RecoveryInfo(stream, this.Name)
                                };
            return extraInfo;
        }

        protected ExtraInformation<T> LoadInfoFile(Guid id, bool deleteOnClose)
        {
            try
            {
                FileOptions fileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan;
                if (deleteOnClose)
                {
                    fileOptions |= FileOptions.DeleteOnClose;
                }

                string fileName = this.GetItemInfoFileName(id);
                var file = new FileInfo(fileName);
                if (deleteOnClose)
                {
                    fileName = this.GetRemovingTempItemInfoFileName(id);
                    file.MoveTo(fileName);
                }

                if (File.Exists(fileName))
                {
                    var streamInfoFile =
                        new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, this.BufferSize, fileOptions);
                    var binForm = new BinaryFormatter();
                    var info = (ExtraInformation<T>) binForm.Deserialize(streamInfoFile);
                    streamInfoFile.Close();
                    return info;
                }

                return default(ExtraInformation<T>);
            }
            catch (IOException ex)
            {
                Trace.Fail(ex.Message, ex.ToString());
            }

            return default(ExtraInformation<T>);
        }

        protected FileStream LoadDataFile(Guid id, bool deleteOnClose)
        {
            FileOptions fileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan;
            if (deleteOnClose)
            {
                fileOptions |= FileOptions.DeleteOnClose;
            }

            string fileName = this.GetItemFileName(id);
            var file = new FileInfo(fileName);
            if (deleteOnClose)
            {
                fileName = this.GetRemovingTempItemFileName(id);
                file.MoveTo(fileName);
            }

            return new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, this.BufferSize, fileOptions);
        }

        protected void RemoveDataFile(Guid id)
        {
            this.LoadDataFile(id, true).Close();
        }

        protected void RemoveInfoFile(Guid id)
        {
            this.LoadInfoFile(id, true);
        }

        protected object ReadHeader(Header[] headers)
        {
            foreach (Header header in headers)
            {
                switch (header.Name)
                {
                    case HeaderCollectionName:
                        this.collectionName = (string)header.Value;
                        break;
                    case HeaderVersion:
                        this.Version = (int)header.Value;
                        break;
                    case HeaderCollectionType:
                        this.CollectionType = (string)header.Value;
                        break;
                }
            }

            return null;
        }

        protected void SaveDataFile(Guid id, Stream stream)
        {
            this.directoryInfo.Refresh();
            if (!this.directoryInfo.Exists)
            {
                this.directoryInfo.Create();
            }

            string newTempFileName = this.GetNewTempItemFileName(id);

            Stream streamFile = Stream.Synchronized(new FileStream(newTempFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None, this.BufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan));

            this.CopyStream(stream, streamFile);
            streamFile.Close();
            stream.Close();

            var file = new FileInfo(newTempFileName);
            file.MoveTo(this.GetItemFileName(id));
        }

        protected void AppendAndSaveDataFile(Guid id, Stream stream)
        {
            this.directoryInfo.Refresh();
            if (!this.directoryInfo.Exists)
            {
                this.directoryInfo.Create();
            }

            string fileName = this.GetItemFileName(id);

            // abre o arquivo no final para adicionar mais informacoes
            Stream streamFile = Stream.Synchronized(new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.None, this.BufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan));

            this.CopyStream(stream, streamFile);
            streamFile.Close();
            stream.Close();
        }

        protected Stream LoadDataInfo(Guid id, out T info, bool deleteOnClose)
        {
            if (File.Exists(this.GetItemFileName(id)))
            {
                try
                {
                    LoadFileHandler<ExtraInformation<T>> loadInfo = this.LoadInfoFile;

                    LoadFileHandler<FileStream> loadData = this.LoadDataFile;
                    IAsyncResult resultLoadData = loadData.BeginInvoke(id, deleteOnClose, null, null);
                    IAsyncResult resultLoadInfo = loadInfo.BeginInvoke(id, deleteOnClose, null, null);

                    if (this.AutomaticSave)
                    {
                        this.SaveCollection();
                    }

                    ExtraInformation<T> extraInfo = loadInfo.EndInvoke(resultLoadInfo);
                    info = extraInfo.ExtraInfo;

                    FileStream data;
                    data = loadData.EndInvoke(resultLoadData);

                    return data;
                }
                catch (SerializationException ex)
                {
                    Trace.Fail(ex.Message, ex.ToString());
                }
                catch (IOException ex)
                {
                    Trace.Fail(ex.Message, ex.ToString());
                }
            }

            info = default(T);
            return null;
        }

        protected virtual void AddToCollection(Guid id)
        {
        }

        protected virtual void CleanCollection()
        {
        }

        protected virtual Header[] GetHeaders()
        {
            var headers = new Header[3];
            headers[0] = new Header(HeaderCollectionName, this.collectionName, true, "urn:StreamFileCollection");
            headers[1] = new Header(HeaderVersion, 1, true, "urn:StreamFileCollection");
            headers[2] = new Header(HeaderCollectionType, "StreamFileList", true, "urn:StreamFileCollection");
            return headers;
        }

        protected int VerifyFilesInCollection()
        {
            int count = 0;
            foreach (FileInfo fileInfo in IOHelper.GetFileInfos(this.directoryInfo.FullName, string.Format("*.{0};*.{1}.{2}", ExtensionInfo, ExtensionInfo, ExtensionNew), SearchOption.TopDirectoryOnly, FileOrder.CreationTime))
            {
                string strId = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(fileInfo.Name));

                FileInfo dataFileInfo = GetDataFileInfo(fileInfo);
                Guid id;

                try
                {
                    id = new Guid(strId);
                }
                catch (FormatException)
                {
                    // arquivo com o nome no formato invalido de um GUID
                    fileInfo.Delete();
                    dataFileInfo.Delete();
                    count++;

                    Trace.TraceError("The item {0} failed on recovering because the name of the file is not formatted ", strId);

                    continue;
                }

                ExtraInformation<T> extraInfo = this.LoadInfoFile(id, false);

                if (extraInfo != null)
                {
                    // o arquivo pertence a essa fila
                    if (extraInfo.RecoveryInfo.ParentID == this.Name)
                    {
                        if (dataFileInfo.Length != extraInfo.RecoveryInfo.Size)
                        {
                            Trace.TraceError("The item {0} failed on recovering because the size of the data file changed or is corrupted", id);
                            
                            // removendo arquivos corrompidos
                            dataFileInfo.Delete();
                            fileInfo.Delete();
                            count++;
                            
                            // vai para o proximo arquivo
                            continue;
                        }

                        // CALCULA O HASH DO DAT
                        {
                            FileStream dataFileStream = this.LoadDataFile(id, false);
                            string hash = BitConverter.ToString(MD5.Create().ComputeHash(dataFileStream));
                            dataFileStream.Close();

                            if (!hash.Equals(extraInfo.RecoveryInfo.Hash))
                            {
                                Trace.TraceError("The item {0} failed on recovering because the Hash of the data file changed or is corrupted", id);
                                
                                // removendo arquivos corrompidos
                                dataFileInfo.Delete();
                                fileInfo.Delete();
                                count++;
                                continue;
                            }
                        }

                        // TUDO OK ADICIONA NA FILA                            
                        this.AddToCollection(id);
                    }
                }
                else
                {
                    // NORMALMENTE OCORRE QUANDO O OBJETO ARMAZENADO NA INFO NAO EH O MESMO OU FOI MODIFICADA SUA ESTRUTURA
                    Trace.TraceError("The item {0} failed on recovering because the stored information changed or is corrupted", id);
                   
                    // removendo arquivos corrompidos
                    dataFileInfo.Delete();
                    fileInfo.Delete();
                    count++;
                }
            }

            return count;
        }

        /// <summary>
        ///  Limpa os arquivos orfaos
        /// </summary>
        /// <returns>Quantidade de arquivos orfaos encontrados</returns>
        protected int ClearOrpheansFiles()
        {
            int count = 0;

            // remove arquivos Temporarios
            foreach (FileInfo file in this.directoryInfo.GetFiles(string.Format("*.{0}", ExtensionTemp)))
            {
                Trace.TraceWarning("The item {0} removed because it is a temporary file.", Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file.Name)));
                file.Delete();
                count++;
            }

            foreach (FileInfo file in IOHelper.GetFileInfos(this.directoryInfo.FullName, string.Format("*.{0};*.{0}.{1}", ExtensionInfo, ExtensionNew), SearchOption.TopDirectoryOnly))
            {
                if (!GetDataFileInfo(file).Exists)
                {
                    Trace.TraceWarning("The item {0} removed because hasn't a data file.", Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file.Name)));
                    file.Delete();
                    count++;
                }
            }

            // existe dat e nao existe info
            foreach (FileInfo file in IOHelper.GetFileInfos(this.directoryInfo.FullName, string.Format("*.{0};*.{0}.{1}", ExtensionData, ExtensionNew), SearchOption.TopDirectoryOnly))
            {
                if (!GetInfoFileInfo(file).Exists)
                {
                    Trace.TraceWarning("The item {0} removed because hasn't a info file.", Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file.Name)));
                    file.Delete();
                    count++;
                }
            }
         
            return count;
        }

        protected void SaveInfoFile<T>(Guid id, ExtraInformation<T> info)
        {
            if (info != null)
            {
                this.directoryInfo.Refresh();
                if (!this.directoryInfo.Exists)
                {
                    this.directoryInfo.Create();
                }

                string newTempFileName = this.GetNewTempItemInfoFileName(id);

                using (var streamInfoFile = new FileStream(newTempFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None, this.BufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan))
                {
                    var binForm = new BinaryFormatter { TypeFormat = FormatterTypeStyle.TypesWhenNeeded };
                    binForm.Serialize(streamInfoFile, info);
                }
                
                var file = new FileInfo(newTempFileName);
                file.MoveTo(this.GetItemInfoFileName(id));
            }
        }

        #region FileNameFormat

        protected string GetItemFileName(Guid id)
        {
            return string.Format("{0}\\{1}.{2}", this.directoryInfo.FullName, id, ExtensionData);
        }

        protected string GetItemInfoFileName(Guid id)
        {
            return string.Format("{0}\\{1}.{2}", this.directoryInfo.FullName, id, ExtensionInfo);
        }

        protected string GetNewTempItemFileName(Guid id)
        {
            return string.Format("{0}\\{1}.{2}.{3}", this.directoryInfo.FullName, id, ExtensionData, ExtensionNew);
        }

        protected string GetNewTempItemInfoFileName(Guid id)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}.{2}.{3}", this.directoryInfo.FullName, id, ExtensionInfo, ExtensionNew);
        }

        protected string GetRemovingTempItemFileName(Guid id)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}.{2}.{3}", this.directoryInfo.FullName, id, ExtensionData, ExtensionTemp);
        }

        protected string GetRemovingTempItemInfoFileName(Guid id)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}.{2}.{3}", this.directoryInfo.FullName, id, ExtensionInfo, ExtensionTemp);
        }

        #endregion
    }
}